package hotnet.session;

import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

import hotnet.config.IoSessionConfig;
import hotnet.exception.CloseChannelException;
import hotnet.exception.ExceptionMonitor;
import hotnet.filter.DefaultIoFilterChain;
import hotnet.filter.IoFilterChain;
import hotnet.future.CloseFuture;
import hotnet.future.ConstantFutureMessage;
import hotnet.future.DefaultCloseFuture;
import hotnet.future.DefaultWriteFuture;
import hotnet.future.WriteFuture;
import hotnet.handler.IoHandler;
import hotnet.processor.IoProcessor;
import hotnet.service.IoService;

public abstract class AbstractIoSession implements IoSession {
	private WriteFuture CLOSE_REQUEST = new DefaultWriteFuture(
			this, ConstantFutureMessage.closeObject);
	
	private static AtomicLong idGenarator = new AtomicLong(0);
	private final IoProcessor processor;
	private final IoFilterChain chain;
	private final IoHandler handler;

	private long sessionId;
	private CloseFuture closeFuture = new DefaultCloseFuture(this);
	private Object lock = new Object();
	private volatile boolean closing;
	private Queue<WriteFuture> writeFutures = new ConcurrentLinkedQueue<WriteFuture>();
	protected IoSessionConfig config;
	private IoService service;
	private final AtomicBoolean scheduledForFlush = new AtomicBoolean();
	private final IoSessionAttributeMap attributesMap = new DefaultIoSessionAttributeMap();
	 
	public AbstractIoSession(IoProcessor processor, IoService service) {
		this.processor = processor;
		sessionId = idGenarator.incrementAndGet();
		this.service = service;
		this.chain = new DefaultIoFilterChain(this);
		service.getIoFilterChainBuilder().buildFilterChain(this.chain);

		this.handler = service.getHandler();
		if (this.handler == null) {
			throw new IllegalStateException("service's handler is null");
		}
	}
	
	@Override
	public IoHandler getHandler() {
		return this.handler;
	}
	
	@Override
	public long getSessionId() {
		return sessionId;
	}

	@Override
	public IoProcessor getProcessor() {
		return processor;
	}

	@Override
	public IoFilterChain getFilterChain() {
		return chain;
	}

	@Override
	public IoSessionConfig getSessionConfig() {
		return config;
	}

	@Override
	public SocketAddress getLocalAddress() {
		try {
			SocketAddress address = getChannel().getLocalAddress();
			
			return address;
		} catch(ClosedChannelException e) {
			ExceptionMonitor.getInstance().exceptionCaught(e);
			
			return null;
		} catch (Exception e) {
			ExceptionMonitor.getInstance().exceptionCaught(e);
			
			return null;
		}
	}

	@Override
	public SocketAddress getRemoteAddress() {
		try {
			SocketAddress address = getChannel().getRemoteAddress();
			
			return address;
		} catch(ClosedChannelException e) {
			ExceptionMonitor.getInstance().exceptionCaught(e);
			
			return null;
		} catch (Exception e) {
			ExceptionMonitor.getInstance().exceptionCaught(e);
			
			return null;
		}
	}

	public boolean isClosing() {
		return closing || closeFuture.isClosed();
	}

	public boolean isClosed() {
		return closeFuture.isClosed();
	}

	@Override
	public WriteFuture write(Object msg) {
		if (isClosing() || !isConnected()) {
			WriteFuture future = new DefaultWriteFuture(this, msg);
			future.setException(new CloseChannelException("closed channel"));
			
			return future;
		}
		
		/* fire write from tail to head and add writefuture to processor */
		 WriteFuture writeFuture = new DefaultWriteFuture(this, msg);
		 IoFilterChain filterChain = getFilterChain();
	     filterChain.fireFilterWrite(writeFuture);
	     
	     return writeFuture;
	}

	@Override
	public CloseFuture close() {
		synchronized (lock) {
			if (isClosing()) {
				return closeFuture;
			}
			
			closing = true;
		}
		
		getFilterChain().fireFilterClose();
		
		return closeFuture;
	}

	@Override
	public CloseFuture close(boolean rightNow) {
		if (!isClosing()) {
			if (rightNow) {
				return close();
			} else {
				return closeAfterFlush();
			}
		} else {
			return closeFuture;
		}
	}
	
	private CloseFuture closeAfterFlush() {
		writeFutures.offer(CLOSE_REQUEST);
        getProcessor().flush(this);
        
        return closeFuture;
	}

	@Override
	public boolean isConnected() {
		/* for server's connected when session created
		 * for client's connected also when session created 
		 */
		return !closeFuture.isClosed();
	}
	
	public CloseFuture getCloseFuture() {
		return closeFuture;
	}
	
	public IoService getService() {
		return this.service;
	}
	
	public Queue<WriteFuture> getWriteFutureQueue() {
		return writeFutures;
	}
	
    public final boolean isScheduledForFlush() {
        return scheduledForFlush.get();
    }
    
    public final void scheduledForFlush() {
        scheduledForFlush.set(true);
    }
    
    public final void unscheduledForFlush() {
        scheduledForFlush.set(false);
    }
    
    public final boolean setScheduledForFlush(boolean schedule) {
        if (schedule) {
            // If the current tag is set to false, switch it to true,
            // otherwise, we do nothing but return false : the session
            // is already scheduled for flush
            return scheduledForFlush.compareAndSet(false, schedule);
        }

        scheduledForFlush.set(schedule);
        return true;
    }
    
	public Object getAttribute(Object key, Object defaultValue) {
		return attributesMap.getAttribute(key, defaultValue);
	}
	
	public Object setAttribute(Object key, Object value) {
		return attributesMap.setAttribute(key, value);
	}
	
	public Object setAttributeIfAbsent(Object key, Object value) {
		return attributesMap.setAttributeIfAbsent(key, value);
	}
	
	public Object removeAttribute(Object key) {
		return attributesMap.removeAttribute(key);
	}
	
	public boolean removeAttribute(Object key, Object value) {
		return attributesMap.removeAttribute(key, value);
	}
	
	public boolean replaceAttribute(Object key, Object oldValue, Object newValue) {
		return attributesMap.replaceAttribute(key, oldValue, newValue);
	}
	
	public boolean containsAttribute(Object key) {
		return attributesMap.containsAttribute(key);
	}
	
	public Set<Object> getAttributeKeys() {
		return attributesMap.getAttributeKeys();
	}

}
